﻿using System;
using System.Collections.Generic;
using System.Linq;
using Netfraction.Common;

namespace NetfractionHub.Network.Protocol.BASE.Handler
{
	public class HandlerINF : ProtocolMessageActionHandler
	{

		public HandlerINF() : base()
		{
			ParameterCodes = new List<string>(new string[]
			{"ID", "PD", "I4", "I6", "U4", "U6", "SS", "SF", "VE", "US", "DS", "SL", "AS", 
				"AM", "EM", "NI", "DE", "HN", "HR", "HO", "TO", "CT", "AW", "SU", "RF"});

			mSupportedConnectionStates = new List<RemoteMachine.ConnectionState>();
			mSupportedConnectionStates.Add(RemoteMachine.ConnectionState.Identify);
			mSupportedConnectionStates.Add(RemoteMachine.ConnectionState.Normal);
		}

		#region ProtocolMessageActionHandler Members

		public override string ProtocolActionCode
		{
			get { return "INF"; }
		}

		public override bool HandleProtocolMessage(RemoteMachine.Node node, ProtocolMessage message, IEnumerable<ProtocolFeature> extenders)
		{
			if (IsSupportedConnectionState(node))
			{
				if (message.Parameters.Count != 0)
				{
					message.Parameters = DecodeParameters(message.Parameters);

					Dictionary<int, object> outParams = new Dictionary<int, object>();
					outParams.Add((int)Factory.FactoryINF.ParameterEnum.SessionID, node.SessionId);
					try
					{
						foreach (ProtocolParameter param in message.Parameters.Where(c => !string.IsNullOrEmpty(c.ParameterCode)))
						{
							switch (param.ParameterCode)
							{
								case "ID":
									// Only handle Client ID changes when the user connects. It cannot be changed later.
									if (node.Connection.State == NetfractionHub.RemoteMachine.ConnectionState.Identify)
									{
										node.Identity.ClientId = param.ParameterValue;

										node.SecurityIdentity = Security.IdentityManager.GetIdentityByClientId(node.Identity.ClientId);
										if (node.SecurityIdentity == null)
										{
											// Goes through the list of DefaultRole rules in the HubConnection settings. If the connecting IP falls into a range, 
											// then assign it to the matching role. Higher listed rules take precedence.
											Security.Identity genericIdent = null;
											foreach (Settings.HubSettings.HubConnection.Rules.DefaultRole defRole in Settings.HubSettings.HubConnection.Rules.DefaultRoles)
											{
												if (genericIdent == null)
												{
													if (IPHelper.IsIPInRange(node.Connection.RemoteEndPoint.Address, defRole.IPRangeStart, defRole.IPRangeEnd))
													{
														genericIdent = Security.Identity.CreateGenericIdentityForRole(Security.RoleManager.GetRoleById(defRole.RoleId));
														if (defRole.AncestorId != byte.MaxValue)
															genericIdent.Role.Ancestor = Security.RoleManager.GetRoleById(defRole.AncestorId);
													}
												}
											}
											if (genericIdent == null)
											{
												// This could be better... if role 6 (Limited User) doesn't exist, then this will set the default role to role 0 (Owner)
												// However, if a Default Role has been set over all possible IP's in the rules above then this code will never execute.
												// Place the catch-all at the bottom of the rule list so it only happens if the other rules don't match up first.
												// This code must be here because the hub MUST assign a security identity to connecting users.
												// If role 0 (Owner) does not exist, then the function will fail, and the user will not connect.
												Security.Role alternativeRole = Security.RoleManager.GetRoleById(6);
												if (alternativeRole == null)
													alternativeRole = Security.RoleManager.GetRoleById(0);

												genericIdent = Security.Identity.CreateGenericIdentityForRole(alternativeRole);
											}
											genericIdent.ClientId = node.Identity.ClientId;
											node.SecurityIdentity = genericIdent;
										}
										else
										{
											node.Identity.NodeType = IdentityNodeTypes.RegisteredUser;
										}

										// This section needs to be reworked - what if the owner adds an account intended to be some form of operator, the role ID will be 8 or more
										if (node.SecurityIdentity.Role.RoleId == 1)
											node.Identity.NodeType = node.Identity.NodeType | IdentityNodeTypes.HubOwner;

										if (node.SecurityIdentity.Role.RoleId <= 3)
											node.Identity.NodeType = node.Identity.NodeType | IdentityNodeTypes.Operator;
									}
									break;
								case "PD":
									node.Identity.PrivateId = param.ParameterValue;
									break;
								case "I4":
									if (ParseIPv4Address(node, param))
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.IPv4_Address, param.ParameterValue);
									break;
								case "I6":
									if (ParseIPv6Address(node, param))
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.IPv6_Address, param.ParameterValue);
									break;
								case "U4":
									if (ParseUDPv4Port(node, param))
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.UDPv4_Port, param.ParameterValue);
									break;
								case "U6":
									if (ParseUDPv6Port(node, param))
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.UDPv6_Port, param.ParameterValue);
									break;
								case "SS":
									if (Security.Permission.HasPermission(node.SecurityIdentity, typeof(Security.Permissions.SharePermission)))
									{
										node.Identity.ShareSize = Convert.ToInt64(param.ParameterValue);
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.ShareSize, param.ParameterValue);

										// Check if the user meets the minimum share requirements. If not, and they have no override permission, disconnect them.
										// Add a check to see if their role lets them share at all. Mark this as exempt for now. TODO: Decide if this is a good idea.
										// If we don't give them share permission then we should probably block them on some other basis if we don't want them to connect.
										if (node.Identity.ShareSize < Settings.HubSettings.SharingRules.MinimumShareSize
											&& !Security.Permission.HasPermission(node.SecurityIdentity, typeof(Security.Permissions.OverrideMinimumShareSizePermission))
											&& Security.Permission.HasPermission(node.SecurityIdentity, typeof(Security.Permissions.SharePermission)))
										{
											Console.WriteLine("User: {0} blocked due to minimum share rule", node.Connection.RemoteEndPoint.Address.ToString());
											ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.GenericKickBanDisconnect, Settings.HubSettings.SharingRules.MinimumShareMessage);
											ConnectionManager.DisconnectNode(node);
											return false;
										}
									}
									break;
								case "SF":
									if (Security.Permission.HasPermission(node.SecurityIdentity, typeof(Security.Permissions.SharePermission)))
									{
										node.Identity.SharedFilesCount = Convert.ToInt32(param.ParameterValue);
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.SharedFilesCount, param.ParameterValue);
									}
									break;
								case "VE":
									node.Identity.ClientVersion = param.ParameterValue;
									break;
								case "US":
									node.Identity.MaximumUploadSpeed = Convert.ToInt64(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.MaximumUploadSpeed, param.ParameterValue);
									break;
								case "DS":
									node.Identity.MaximumDownloadSpeed = Convert.ToInt64(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.MaximumDownloadSpeed, param.ParameterValue);
									break;
								case "SL":
									node.Identity.UploadSlots = Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.MaximumUploadSlots, param.ParameterValue);
									break;
								case "AS":
									node.Identity.AutomaticSlotAllocatorSpeedLimit = Convert.ToInt64(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.AutomaticSlotAllocatorSpeedLimit, param.ParameterValue);
									break;
								case "AM":
									node.Identity.MinimumUploadConnectionsForAutoSlotAlloc = Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.MinimumUploadConnectionsForAutoSlotAlloc, param.ParameterValue);
									break;
								case "EM":
									node.Identity.Email = param.ParameterValue;
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.EmailAddress, param.ParameterValue);
									break;
								case "NI":
									if (param.ParameterValue.Length >= Settings.HubSettings.NicknameRules.MinimumLength && param.ParameterValue.Length <= Settings.HubSettings.NicknameRules.MaximumLength)
									{
										if (!ContainsUnicodeControlOrSeperator(param.ParameterValue) && System.Text.RegularExpressions.Regex.Match(param.ParameterValue, Settings.HubSettings.NicknameRules.AllowedCharacters, System.Text.RegularExpressions.RegexOptions.IgnorePatternWhitespace).Success)
										{
											node.Identity.Nickname = param.ParameterValue;
											outParams.Add((int)Factory.FactoryINF.ParameterEnum.NickOrHubName, param.ParameterValue);
										}
										else
										{
											// nickname contains invalid characters
											ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.NickInvalid);
											ConnectionManager.DisconnectNode(node);
											return false;
										}
									}
									else
									{
										// nickname length too short or too long
										ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.NickInvalid);
										ConnectionManager.DisconnectNode(node);
										return false;
									}
									break;
								case "DE":
									if (!ContainsUnicodeControlOrSeperator(param.ParameterValue))
									{
										node.Identity.Description = param.ParameterValue;
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.Description, param.ParameterValue);
									}
									else
									{
										// description contains invalid characters
										// will just ignore the parameter, nothing explicit in the spec
									}
									break;
								case "HN":
									node.Identity.HubNormalCount = Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.UserNormalCount, param.ParameterValue);
									break;
								case "HR":
									node.Identity.HubRegisteredCount = Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.UserRegisteredCount, param.ParameterValue);
									break;
								case "HO":
									node.Identity.HubOperatorCount = Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.UserOperatorCount, param.ParameterValue);
									break;
								case "TO":
									// hub shouldn't receive this?
									node.Identity.Token = param.ParameterValue;
									break;
								case "CT":
									node.Identity.NodeType = (IdentityNodeTypes)Convert.ToInt32(param.ParameterValue);
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.ClientType, param.ParameterValue);
									break;
								case "AW":
									node.Identity.AwayStatus = param.ParameterValue != string.Empty ? ((IdentityAwayStatus)Convert.ToInt32(param.ParameterValue)) : IdentityAwayStatus.Online;
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.AwayStatus, param.ParameterValue);
									break;
								case "SU":
									if (param.ParameterValue == string.Empty)
										node.Identity.ClientExtendedFeatures.Clear();
									else
									{
										foreach (string feat in param.ParameterValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
										{
											node.Identity.ClientExtendedFeatures.Add(feat);
										}
										outParams.Add((int)Factory.FactoryINF.ParameterEnum.SupportedFeatures, param.ParameterValue);
									}
									break;
								case "RF":
									node.Identity.ReferrerUrl = param.ParameterValue;
									outParams.Add((int)Factory.FactoryINF.ParameterEnum.RefererURL, param.ParameterValue);
									break;
								default:
									break;
							}
						}
						node.Identity.UpdateBroadcastInfoString(node.SessionId);
					}
					catch (FormatException ex)
					{
						System.Diagnostics.Debug.WriteLine(ex.Message);
						Console.WriteLine(ex.Message);
					}
					catch (Exception e)
					{
						System.Diagnostics.Debug.WriteLine(e.Message);
					}

					switch (node.Connection.State)
					{
						case NetfractionHub.RemoteMachine.ConnectionState.Identify:
							//lock (node)
							//{
								if (!string.IsNullOrEmpty(node.Identity.ClientId))
								{
									if (node.Identity.ClientId != ConnectionManager.GenerateClientId(node, Base32.Decode(node.Identity.PrivateId)))
									{
										ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.InvalidPrivateID);
										ConnectionManager.DisconnectNode(node);
										return false;
									}

									if (ConnectionManager.GetConnectedNodeByClientId(node.Identity.ClientId) != null)
									{
										Console.WriteLine("disconnect - CID already in use");
										ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.ClientIDTaken);
										node.Connection.Disconnect();
										return false;
									}
								}
								else
								{
									Console.WriteLine("disconnect - must have a CID");
									// should return a flag, FM
									ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.RequiredINFFieldMissingOrBad);
									return false;
								}

								if (!string.IsNullOrEmpty(node.Identity.Nickname))
								{
									if (ConnectionManager.GetConnectedNodeByNickname(node.Identity.Nickname) != null)
									{
										Console.WriteLine("disconnect - nick already in use");
										ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.NickTaken);
										return false;
									}

									// NI handler above contains code for invalid nicks
								}
								else
								{
									Console.WriteLine("disconnect - must have a nick");
									// StatusErrorCode.RequiredINFFieldMissingOrBad is more accurate, return FM flag too
									ConnectionManager.SendStatusMessageToNode(node, Factory.FactorySTA.StatusSeverity.FatalError, Factory.FactorySTA.StatusErrorCode.NickInvalid);
									return false;
								}
								

								/*node.SecurityIdentity = Security.IdentityManager.GetIdentityByClientId(node.Identity.ClientId);
								if (node.SecurityIdentity == null)
								{
									Security.Identity genericIdent = Security.Identity.CreateGenericIdentityForRole(Security.RoleManager.GetRoleByName("Normal User"));
									genericIdent.ClientId = node.Identity.ClientId;
									node.SecurityIdentity = genericIdent;
								}*/

								if (node.SecurityIdentity.Password != string.Empty) // password required, send IGPA
								{
									//ConnectionManager.SendGetPasswordRequestToNode(node);
									Random rand = new Random();
									node.Identity.PasswordSaltBytes = new byte[24];
									rand.NextBytes(node.Identity.PasswordSaltBytes);

									Protocol.ProtocolMessageFactory factory = new Protocol.BASE.Factory.FactoryGPA();
									Dictionary<int, object> factoryParams = new Dictionary<int, object>();
									factoryParams.Add((int)Protocol.BASE.Factory.FactoryGPA.ParameterEnum.Data, Base32.Encode(node.Identity.PasswordSaltBytes).Substring(0, 39));

									node.Connection.Send(factory.Create(Protocol.ProtocolMessage.MessageTypeEnum.Info, factoryParams).ToString());

									node.Connection.State = NetfractionHub.RemoteMachine.ConnectionState.Verify;
								}
								else
								{
									// no password, just send INF's
									ConnectionManager.OnClientConnectSuccess(node);
								}
							//}
							break;
						case NetfractionHub.RemoteMachine.ConnectionState.Normal:
							if (outParams.Count > 1)
								ConnectionManager.SendNodeInformationUpdateToAll(outParams);

							break;
						default:
							return false;
					}
					return true;
				}
			}
			return false;
		}

		private static bool ContainsUnicodeControlOrSeperator(string text)
		{
			return !System.Text.RegularExpressions.Regex.Match(text, @"^[^\p{Cc}\p{Z}]+$").Success;
		}

		private static bool ParseUDPv6Port(RemoteMachine.Node node, ProtocolParameter param)
		{
			int udp6Port = 0;
			if (!int.TryParse(param.ParameterValue, out udp6Port))
				return false;

			if (udp6Port >= System.Net.IPEndPoint.MinPort && udp6Port < System.Net.IPEndPoint.MaxPort)
			{
				node.Identity.Udp6Port = udp6Port;
				return true;
			}
			return false;
		}
		private static bool ParseUDPv4Port(RemoteMachine.Node node, ProtocolParameter param)
		{
			int udp4Port = 0;
			if (!int.TryParse(param.ParameterValue, out udp4Port))
				return false;

			if (udp4Port >= System.Net.IPEndPoint.MinPort && udp4Port < System.Net.IPEndPoint.MaxPort)
			{
				node.Identity.Udp4Port = udp4Port;
				return true;
			}
			return false;
		}

		private static bool ParseIPv6Address(RemoteMachine.Node node, ProtocolParameter param)
		{
			// no idea if this code works, don't have an IPv6 network to test on
			System.Net.IPAddress ipv6Holder = null;
			if (!System.Net.IPAddress.TryParse(param.ParameterValue, out ipv6Holder))
				return false;

			if (System.Net.IPAddress.Equals(ipv6Holder, System.Net.IPAddress.IPv6None) || System.Net.IPAddress.Equals(ipv6Holder, System.Net.IPAddress.IPv6Any) || System.Net.IPAddress.Equals(ipv6Holder, System.Net.IPAddress.IPv6Loopback))
				node.Identity.IPv6Address = node.Connection.RemoteEndPoint.Address;
			else
				node.Identity.IPv6Address = ipv6Holder;

			if (!System.Net.IPAddress.Equals(node.Identity.IPv6Address, node.Connection.RemoteEndPoint.Address))
				Console.WriteLine("IP recv'd not equal to source IP");  // send STA //return false;

			return true;
		}

		private static bool ParseIPv4Address(RemoteMachine.Node node, ProtocolParameter param)
		{
			System.Net.IPAddress ipHolder = null;
			if (!System.Net.IPAddress.TryParse(param.ParameterValue, out ipHolder))
				return false;

			if (System.Net.IPAddress.Equals(ipHolder, System.Net.IPAddress.None) || System.Net.IPAddress.Equals(ipHolder, System.Net.IPAddress.Any) || System.Net.IPAddress.Equals(ipHolder, System.Net.IPAddress.Loopback))
				node.Identity.IPv4Address = node.Connection.RemoteEndPoint.Address;
			else
				node.Identity.IPv4Address = ipHolder;

			if (!System.Net.IPAddress.Equals(node.Identity.IPv4Address, node.Connection.RemoteEndPoint.Address))
			{
				Console.WriteLine("IP recv'd not equal to source IP");  // send STA
				node.Identity.IPv4Address = node.Connection.RemoteEndPoint.Address;
				//return false;
			}
			return true;
		}

		#endregion
	}
}
